#!/usr/bin/python-3.1
# -*- coding: utf-8 -*-

# Imports

from types import FunctionType

# Constants

# All styles that can be inherited from parents
inherit = tuple()

# Description of an XML file

class XMLFile(list):

    def __init__(self, url):
        with open(url) as file:
            super(XMLFile, self).__init__(file.read())
        self.cursor = 0

    def read(self, length=0):
        start = self.cursor
        end = self.cursor + length if length else -1
        self.cursor = end
        return self[start, end]

    def write(self, text, start=None):
        if start is not None:
            self.cursor = start
        self[self.cursor:self.cursor] = text

    def stream(self, step=1, overlap=0):
        base = 0
        netstep = step - overlap
        while base + step < len(self):
            yield base, self[base:base+step]
            base += netstep

    def find(self, tofind, maximum=-1):
        # Important : add support for regular expressions
        tofind = list(tofind)
        start, end = 0, len(tofind)
        found = 0
        for index, text in self.stream(len(tofind), len(tofind)-1):
            if text == tofind:
                yield slice(index, index+len(tofind))
                found += 1

    def replace(self, tofind, text, maximum=-1):
        for index in self.find(tofind, maximum):
            self[index] = text

    def __repr__(self):
        return '<XML File object : ' +\
               repr(''.join(self[0:6]))[:-1] + ' [...] ' +\
               repr(''.join(self[-5:]))[1:] + '>'

    def __str__(self):
        return ''.join(self)


# Description of an XML parser

class Name:

    def __init__(self, name, space=''):
        self.name = name
        self.space = space

    def __repr__(self):
        return '<XML Element Name ' + self.space + ':' + self.name +\
               ' at ' + str(id(self)) + ' >'

    def __str__(self):
        if self.space:
            return self.space + ':' + self.name
        else:
            return self.name

    def __eq__(self, other):
        if isinstance(other, str):
            return other == self.name
        elif isinstance(other, Name):
            return other.name == self.name and other.space == self.space

    def __hash__(self):
        return hash(str(self))

class Node(list):

    def __init__(self):
        # Variables used for normal behavior, as root of the DOM tree
        self.name = Name('')
        self.model = {'id':Name('id', 'xml')}
        self.classes = []
        self.attrs = {}
        self.states = []
        self.parts = {}
        self.styles = {}
        self.parent = self

    def __getitem__(self, index):
        if isinstance(index, (tuple, list)):
            current = self
            for level in index:
                current = current[level]
            return current
        elif isinstance(index, (str, dict, FunctionType)):
            parent = Node()
            parent.extend(self.filter(cond, -1))
            return parent
        else:
            return super().__getitem__(index)

    def __setitem__(self, index, value):
        if isinstance(index, (tuple, list)):
            current = self
            for level in index[:-1]:
                current = current[level]
            current[index[-1]] = value
        elif isinstance(index, (str, dict, FunctionType)):
            indexes = [i for i in self.find(index, -1)]
            for index in indexes:
                self[index] = value
        else:
            super().__setitem__(index, value)

    def __delitem__(self, index):
        if isinstance(index, (tuple, list)):
            current = self
            for level in index[:-1]:
                current = current[level]
            del current[index[-1]]
        elif isinstance(index, (str, dict, FunctionType)):
            indexes = [i for i in self.find(index, -1)]
            for index in indexes:
                del self[index]
        else:
            super().__delitem__(index)

    def __repr__(self):
        return '<XML Node ' + str(self.name) + ' at ' + hex(id(self)) + '>'

    def children(self, cond=lambda x: True, walk=0):
        for child in self.filter(cond, walk):
            if isinstance(child, Element):
                yield child

    def siblings(self, cond=lambda x: True):
        for sibling in self.parent.children(cond):
            if isinstance(sibling, Element) and sibling is not self:
                yield sibling

    def preceding(self, cond=lambda x: True):
        if not isinstance(cond, FunctionType):
            cond = Selectors(cond)
        last = None
        for sibling in self.parent.children():
            if sibling is self:
                return last
            elif cond(sibling):
                last = sibling
        return None

    def following(self, cond=lambda x: True):
        if not isinstance(cond, FunctionType):
            cond = Selectors(cond)
        over = False 
        for sibling in self.parent.children():
            if sibling is self:
                over = True
            elif cond(sibling) and over:
                return sibling
        return None

    def brothers(self, cond=lambda x: True):
        if not isinstance(cond, FunctionType):
            cond = Selectors(cond)
        return [self.preceding(cond), self.following(cond)]

    def ancestors(self, cond=lambda x: True, walk=-1):
        if not isinstance(cond, FunctionType):
            cond = Selectors(cond)
        ancestor = self
        while True:
            new = ancestor.parent
            if new is not ancestor and walk != 0:
                walk -= 1
                ancestor = new
                if cond(ancestor):
                    yield ancestor
            else:
                break

    def find(self, cond=lambda x: True, walk=0):
        if not isinstance(cond, FunctionType):
            cond = Selectors(cond)
        # Using the function to determine which elements to find.
        for index, node in enumerate(self):
            if cond(node):
                yield (index,)
            if walk and isinstance(node, Node):
                for kid in node.find(cond, walk-1):
                    yield (index,) + kid

    def filter(self, cond=lambda x: True, walk=0):
        for index in self.find(cond, walk):
            yield self[index]

    def replace(self, cond=lambda x: True, by=lambda x: x, walk=False):
        for index in self.find(cond, walk):
            self[index] = by(self[index])

    def style(self, style='', value='', **kargs):
        if style and not value:
            yield self.styles[style]
        elif style and value:
            self.styles[style] = value
            yield self.styles[style]
        elif kargs:
            for style in kargs:
                self.styles[style] = kargs[style]
                yield self.styles[style]

    def applystyles(self, cond=lambda x: True, styles={}, walk=-1):
        if not isinstance(cond, FunctionType):
            cond = Selectors(cond)
        if cond(self):
            self.style(**styles)
        for element in self.children(cond, walk):
            element.style(**styles)

    def empty(self):
        del self[:]

    @property
    def id(self, value=''):
        idattr = self.model.get('id', None)
        if value:
            self.attr(idattr, value)
        else:
            return self.attr(idattr)

    def attr(self, name, value=''):
        if value:
            self.attrs[name] = value
        else:
            return self.attrs.get(name, None)

class Parser(Node):

    def __init__(self, xmlfile=''):
        super().__init__()
        # Start parsing the source file
        if xmlfile:
            self(xmlfile)

    def newtag(self, stream, ancestors):
        char = next(stream)
        if char == '!':
            self.newdecl(stream, ancestors)
        elif char == '?':
            self.newpi(stream, ancestors)
        elif char == '/':
            self.endelement(stream, ancestors)
        else:
            self.newelement(stream, char, ancestors)

    def newattr(self, stream, key, attrs):
        # Whoa! We just collected an attribute name!
        # Let's get its value, too.
        # Support for attribute name space will have to be added.
        for char in stream:
            # There may be spaces here, so we just
            # ignore them.
            if char == '"':
                break
        self.newval(stream, key, attrs)

    def newval(self, stream, key, attrs):
        data = ''
        for char in stream:
            if char == '\\':
                # This allows for escaped character.
                # It will be extended for Unicode
                # in the future.
                data = next(stream)
            elif char == '"':
                break
            else:
                data += char
        attrs[key] = data

    def newelement(self, stream, data, ancestors):
        # Tag parsing layer.
        # The tag may be opening an element, or
        # being autoclosed.
        space = name = ''
        attrs = {}
        keyspace = ''
        for char in stream:
            if char == ':':
                if not name:
                    space = data
                else:
                    keyspace = data
                data = ''
            elif char == ' ':
                # Unless the name is not yet defined, a space
                # is not something useful.
                if not name:
                    name, data = data, ''
            elif char == '=':
                key = Name(data, keyspace)
                self.newattr(stream, key, attrs)
                data = ''
            elif char == '/':
                # Oh, this tag is self-closed.
                for char in stream:
                    # There may be spaces here, so we just
                    # ignore them.
                    if char == '>':
                        break
                # Create the name object for the element,
                # composed of his namespace and name.
                name = Name(name, space)
                # Create the new Element node, and append it
                # to the end of its parent, since it's already closed.
                new = Element(name, attrs, ancestors[-1])
                ancestors[-1].append(new)
                break
            elif char == '>':
                # End of the opening tag
                # Create the name object for the element,
                # composed of his namespace and name.
                name = Name(name, space)
                # Create the new Element node, and append it
                # to the list of open tags.
                new = Element(name, attrs, ancestors[-1])
                ancestors.append(new)
                break
            else:
                data += char

    def endelement(self, stream, ancestors):
        # Tag parsing layer, for closing tags.
        data = space = name = ''
        for char in stream:
            if char == ':':
                space, data = data, ''
            elif char == '>':
                name, data = data, ''
                # As this becomes a validating and
                # error-correcting parser, error handling
                # will need to be added here.
                new = ancestors.pop()
                ancestors[-1].append(new)
                break
            else:
                data += char

    def newpi(self, stream, ancestors):
        # Processing instruction layer
        data = name = ''
        attrs, key = {}, ''
        for char in stream:
            if char == ' ':
                # Unless the name is not yet defined, a space
                # is not something useful.
                if not name:
                    name, data = data, ''
            elif char == '=':
                self.newattr(stream, data, attrs)
                data = ''
            elif char == '?':
                # Oh, we reached the end of the P.I.
                for char in stream:
                    # There may be spaces here, so we just
                    # ignore them.
                    if char == '>':
                        break
                new = ProcessingInstruction(name, attrs)
                ancestors[-1].append(new)
                break
            else:
                data += char

    def newdecl(self, stream, ancestors):
        # SGML tagparsing layer.
        # Includes DOCTYPEs, comments, entity decl., etc.
        # It might be useful to create a document type from all
        # declarations found in the document.
        # Further development in that direction will be done.
        data = name = content = ''
        for char in stream:
            if char == ' ':
                # Name separation, if name has not already been set.
                # Meaningless otherwise (might become meaningful
                # eventually).
                if not name:
                    name, data = data, ''
            elif char == '[':
                # This section will eventually allow for
                # in-document doctypes.
                data += char + self.declcontent(stream)
            elif char == '>':
                # End of the SGML tag
                # Right now, a generic SGML class is used,
                # but that will be changed when the need for a
                # doctype comes.
                content = data
                new = SGML(name, content)
                ancestors[-1].append(new)
                break
            else:
                data += char

    def newdoctype(self, stream, ancestors):
        pass

    def newcmodel(self, stream, ancestors):
        pass

    def newattlist(self, stream, ancestors):
        pass

    def newentdef(self, stream, ancestors):
        pass

    def newnotation(self, stream, ancestors):
        pass

    def newcomment(self, stream, ancestors):
        pass

    def declcontent(self, stream):
        data = ''
        for char in stream:
            data += char
            if char == ']':
                break
            elif char == '[':
                data += self.declcontent(stream)
        return data

    def newtext(self, ancestors, text):
        if text:
            ancestors[-1].append(text)

    def newentref(self, stream, ancestors):
        name = ''
        for char in stream:
            if char == ';':
                break
            else:
                name += char
        new = EntityReference(name)
        ancestors[-1].append(new)

    def __call__(self, source):
        if len(self):
            self.empty()
        # Temporary accumulated data (characters)
        data = ''
        # The last element in the list is the one to append new elements to
        ancestors = [self]
        # The source characters generator
        stream = (j for j in source)
        for char in stream:
            # Basic parsing layer, to detect any context to get into
            if char == '<':
                self.newtext(ancestors, data)
                self.newtag(stream, ancestors)
                data = ''
            elif char == '&':
                # Entity reference layer
                self.newtext(ancestors, data)
                self.newentref(stream, ancestors)
                data = ''
            else:
                data += char

    def __repr__(self):
        return '<XML Parser at ' + str(id(self)) + ' >'


class Element(Node):

    def __init__(self, name, attrs={}, parent=None):
        super().__init__()
        self.name = name
        self.attrs = attrs
        self.parent = parent

class SGML(Node):

    def __init__(self, name, content):
        self.name = name
        self.content = content

class DocumentType(SGML):

    def __init__(self, root, location=None):
        pass

class ElementType(SGML):

    def __init__(self, name, content, attrs={}):
        pass

class EntityDefinition(SGML):

    def __init__(self, name, value):
        pass

class NotationDefinition(SGML):
    pass

class Comment(SGML):
    pass

class ProcessingInstruction(Node):

    def __init__(self, name, attrs={}):
        self.name = name
        self.attrs = attrs

class EntityReference(Node):

    def __init__(self, name):
        self.name = name

# Filtering functions for CSS selectors
class Selectors(list):

    def __init__(self, selector):
        self.switches = {'#': self.setid,
                         '.': self.setclass,
                         '[': self.setattribute,
                         ':': self.setpseudo,
                         '>': self.setchild,
                         '~': self.setsibling,
                         '+': self.setbrother,
                         ' ': self.setdescendant,
                         ',': self.newselector}
        if isinstance(selector, str):
            self.feed(selector)
        elif isinstance(selector, list):
            self[:] = selector
        elif isinstance(selector, dict):
            self.append(selector)

    def __call__(self, node):
        return self.func(node)

    def __eq__(self, other):
        if isinstance(other, Element):
            return self(Element)
        elif isinstance(other, Selectors):
            return super().__eq__(other)

    # Creates a new selector in the selectors list
    def newselector(self, stream):
        self.append({})
        self.setname(stream)

    # Set the name of the element
    def setname(self, stream):
        data = ''
        for char in stream:
            if char in self.switches:
                if data:
                    self[-1]['name'] = Name(data)
                self.switches[char](stream)
            else:
                data += char
        if data:
            self[-1]['name'] = data

    # The function working inside the id-finder
    def setid(self, stream):
        data = ''
        for char in stream:
            if char in self.switches:
                if data:
                    self[-1]['id'] = data
                self.switches[char](stream)
            else:
                data += char
        if data:
            self[-1]['id'] = data

    # The function working inside the class finder
    def setclass(self, stream):
        if 'class' not in self[-1]:
            self[-1]['class'] = []
        data = ''
        for char in stream:
            if char in self.switches:
                if data:
                    self[-1]['class'].append(data)
                self.switches[char](stream)
            else:
                data += char
        if data:
            self[-1]['class'].append(data)

    # The function working inside the attribute finder
    def setattribute(self, stream):
        if 'attrs' not in self[-1]:
            self[-1]['attrs'] = {}
        data = ''
        for char in stream:
            # Currently, this does not consider attribute values,
            # only attribute names. This issue will be addressed in further
            # releases.
            if char == ']':
                if data:
                    self[-1]['attrs'][Name(data)] = None
            else:
                data += char
        char = next(stream)
        if char in self.switches:
            self.switches[char](stream)

    # This function works inside the pseudo element and class finder
    def setpseudo(self, stream):
        char = next(stream)
        if char == ':':
            key, data = 'pseudoelement', ''
        else:
            key, data = 'pseudoclass', char
        if key not in self[-1]:
            self[-1][key] = []
        for char in stream:
            if char in self.switches:
                if data:
                    self[-1][key].append(data)
                    data = ''
                self.switches[char](stream)
            else:
                data += char
        if data:
            self[-1][key].append(data)

    # Set what parents the target element has
    def setchild(self, stream):
        parent = self.pop(-1)
        new = {'parent':parent}
        self.append(new)
        self.setname(stream)

    # Set what sibling the target element has
    def setsibling(self, stream):
        sibling = self.pop(-1)
        new = {'sibling':sibling}
        self.append(new)
        self.setname(stream)

    # Set what element is adjacent to the target element
    def setbrother(self, stream):
        brother = self.pop(-1)
        new = {'brother':brother}
        self.append(new)
        self.setname(stream)

    # Set the ancestors of the target element
    def setdescendant(self, stream):
        ancestor = self.pop(-1)
        new = {'ancestor':ancestor}
        self.append(new)
        self.setname(stream)

    # The factory made filter function
    def func(self, node):
        if isinstance(node, Element):
            for selector in self:
                end = True
                if selector.get('name', False) != getattr(node, 'name', True):
                    end = False
                if 'class' in selector:
                    for classname in selector['class']:
                        if classname not in getattr(node, 'classes', []):
                            end = False
                if 'attr' in selector:
                    for attr in selector['attr']:
                        pass
                if end:
                    return True
        return False

    # Initiate the string to function transformation
    def feed(self, selector):
        # Actual code.
        stream = (i for i in selector)
        self.newselector(stream)

if __name__ == '__main__':
    ## Active testing for the parser.
    file = XMLFile('source.xml')
    print('## == Now, a parser show off == ##')
    from timetools import Timer
    parser = Parser()
    timer = Timer()
    print('Let\'s go, New Parser!')
    for i in range(10):
        timer.start()
        parser(file)
        timer.stop()
    print(timer)
    print('The average is of', timer.totaltime/10)
    print()
    print('## == Trying to filter out an element. == ##')
    name = input('What element do you want to look for?')
    print()
    print('# Using a CSS selector :')
    for i in parser.filter(name, -1):
        print(i)
    a = input('Continue?')
    print()
    print('# Using a parameter dictionnary :')
    for i in parser.filter({'name':name}, -1):
        print(i)
    a = input('Continue?')
    print()
    print('# Using a filtering function :')
    for i in parser.filter(lambda x: isinstance(x, Element) and\
                                     x.name == name, -1):
        print(i)
    
